package br.edu.ufcg.greengrid.model;

import java.util.List;

import br.edu.ufcg.greengrid.event.JobSubmissionEvent;
import br.edu.ufcg.greengrid.event.MachineAvailabilityEvent;
import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.job.JobFactory;
import br.edu.ufcg.greengrid.model.job.JobFactoryImpl;

public class Simulator {

	public static JobFactory DEFAULT_JOB_FACTORY = new JobFactoryImpl(Job.DEFAULT_TIME_TO_OPERATIONS_FACTOR);
	
	private Grid grid;

	private List<MachineAvailabilityEvent> machineAvailabilityWorkload;
	private long machineAvailabilityWorkloadDelta;
	private List<JobSubmissionEvent> gridWorkload;

	private int machineAvailabilityWorkloadIndex;
	private int gridWorkloadIndex;

	private MachineAvailabilityEvent cMachineAvailabilityEvent;
	private JobSubmissionEvent cJobSubmissionEvent;
	
	private JobFactory jobFactory;

	private long clock;

	//The number of jobs arrived in the current clock
	private int numJobsArrivedInLastTurn;

	public Simulator(Grid grid, List<MachineAvailabilityEvent> machineAvailabilityWorkload,
			List<JobSubmissionEvent> gridWorkload) {
		this(grid, machineAvailabilityWorkload, gridWorkload, DEFAULT_JOB_FACTORY);
	}
	
	public Simulator(Grid grid, List<MachineAvailabilityEvent> machineAvailabilityWorkload,
			List<JobSubmissionEvent> gridWorkload, JobFactory jobFactory) {
		this.machineAvailabilityWorkload = machineAvailabilityWorkload;
		this.machineAvailabilityWorkloadDelta = 0L;
		this.gridWorkload = gridWorkload;
		this.grid = grid;
		this.jobFactory= jobFactory;
		checkSanity();
		initClock();
	}

	private void initClock() {
		this.clock = this.machineAvailabilityWorkload.get(0).getTimestamp();
	}

	private void checkSanity() {
		if(!this.gridWorkload.isEmpty()){
			long timestamp = this.machineAvailabilityWorkload.get(0).getTimestamp();
			long submittime = this.gridWorkload.get(0).getSubmittime();
			if(timestamp > submittime){
				throw new IllegalArgumentException("Error! First job occurs before the first idle machine appears.");
			}
		}
		if(this.machineAvailabilityWorkload.isEmpty()){
			throw new IllegalArgumentException("Error! There is no machine availability event.");
		}
	}

	public void nextStep() {

		// Update machineAvailabilityEvent if needed
		if (cMachineAvailabilityEvent == null && hasNextMachineAvailabilityEvent()) {
			cMachineAvailabilityEvent = getNextMachineAvailabilityEvent();
		}

		// Update cJobSubmissionEvent if needed
		if (cJobSubmissionEvent == null && hasNextJobSubmissionEvent()) {
			cJobSubmissionEvent = getNextJobSubmissionEvent();
		}

		// Handle MachineAvailabilityEvents that occur in the current clock
		if (clock == cMachineAvailabilityEvent.getTimestamp()) {
			this.grid.setIdleMachine(cMachineAvailabilityEvent.getId(), cMachineAvailabilityEvent.getIdletime());
			while (hasNextMachineAvailabilityEvent()
					&& (cMachineAvailabilityEvent = getNextMachineAvailabilityEvent()).getTimestamp() == clock) {
				this.grid.setIdleMachine(cMachineAvailabilityEvent.getId(), cMachineAvailabilityEvent.getIdletime());
			}
		}

		// Handle JobSubmissionEventEvents that occur in the current clock
		this.numJobsArrivedInLastTurn = 0;
		if (clock == cJobSubmissionEvent.getSubmittime()) {
			this.grid.addJob(getJob(cJobSubmissionEvent));
			this.numJobsArrivedInLastTurn++;
			while (hasNextJobSubmissionEvent()
					&& (cJobSubmissionEvent = getNextJobSubmissionEvent()).getSubmittime() == clock) {
				this.grid.addJob(getJob(cJobSubmissionEvent));
				this.numJobsArrivedInLastTurn++;
			}
		}

		// Update grid state
		this.grid.update();
		clock++;

	}

	private Job getJob(JobSubmissionEvent jobSubmissionEvent) {
		return this.jobFactory.getJob(jobSubmissionEvent.getId(), this.clock, jobSubmissionEvent.getRuntime());
	}

	private boolean hasNextJobSubmissionEvent() {
		return this.gridWorkloadIndex < this.gridWorkload.size();
	}

	private JobSubmissionEvent getNextJobSubmissionEvent() {
		return this.gridWorkload.get(this.gridWorkloadIndex++);
	}

	protected boolean hasNextMachineAvailabilityEvent() {

		return true;
	}

	protected MachineAvailabilityEvent getNextMachineAvailabilityEvent() {
		
		MachineAvailabilityEvent event = this.machineAvailabilityWorkload.get(this.machineAvailabilityWorkloadIndex++);
		
		event.incTimestamp(this.machineAvailabilityWorkloadDelta);
		
		if(this.machineAvailabilityWorkloadIndex >= this.machineAvailabilityWorkload.size()){
			this.machineAvailabilityWorkloadDelta = event.getTimestamp() - this.machineAvailabilityWorkload.get(0).getTimestamp() + 1;
			this.machineAvailabilityWorkloadIndex = 0;
		}
		
		return event.clone();
	}

	public boolean done() {
		return this.gridWorkloadIndex >= this.gridWorkload.size() 
		&& this.grid.done();
	}

	public Grid getGrid() {
		return grid;
	}

	public long getClock() {
		return Math.max(0, this.clock - 1);
	}

	public int getMachinesNotBeingUsedByUser() {
		return this.grid.getNumMachinesNotBeingUsedByUser();
	}

	public int getGridDemand() {
		return this.grid.getGridDemand();
	}

	public int getNumJobsArrivedInLastTurn() {
		return this.numJobsArrivedInLastTurn;
	}
	
	

}
